package com.progenies.ecg.asm31.model.codeparts;

import java.lang.reflect.Method;
import java.util.Iterator;

import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

import com.progenies.ecg.asm31.util.BytecodeInstructionsUtils;
import com.progenies.ecg.asm31.util.VariableRegister;
import com.progenies.ecg.generator.ClassGeneratorFactory;
import com.progenies.ecg.model.AbstractCodeObject;
import com.progenies.ecg.model.ValueReference;
import com.progenies.ecg.model.codeparts.CodeBlock;
import com.progenies.ecg.model.codeparts.ForLoopCodePart;
import com.progenies.ecg.model.codeparts.lines.LineCodePart;
import com.progenies.ecg.model.conditions.BooleanCondition;
import com.progenies.ecg.model.conditions.ICondition;

public class ForLoopCodePartASM31 extends ForLoopCodePart implements IConfigurableCodePart, ILoopASM31
{
	protected MethodVisitor visitor;
	protected AbstractCodeObject parent;
	protected VariableRegister varRegister;
	
	
	protected Label startLabel;
	protected Label postIterateLabel;
	protected Label endLabel;
	private Class<?> variableType;
	private String variableName;
	

	public ForLoopCodePartASM31(LineCodePart initialCode, ICondition condition,	LineCodePart postCode, CodeBlock codeBlock) {
		this.initialInstruction=initialCode;
		this.condition=condition;
		this.postIterationInstruction=postCode;
		this.bodyBlock=codeBlock;
	}

	public ForLoopCodePartASM31(Class<?> variableType, String variableName, ValueReference<? extends Object> iterableVariable, CodeBlock codeBlock) {
		this.variableType=variableType;
		this.variableName=variableName;
		this.iterableCollection=iterableVariable;
		this.bodyBlock=codeBlock;
	}

	@Override
	public void generateBytecode()
	{
		//first, what type of for loop is this object
		if(this.iterableCollection!=null)
		{
			iterateForEach();
		}
		else
		{
			iterateByCondition();
		}

	}

	private void iterateForEach()
	{
		//insert iterableCollection into Stack
		Type resultType=BytecodeInstructionsUtils.insertValueReferenceIntoStack(visitor, iterableCollection, varRegister, this);
		
		ICondition forEachCondition=null;
		IConfigurableCodePart postExecution=null;

		//local variable name, for iterator or counter
		String randomName="iterator_"+System.currentTimeMillis()+"_"+((int)Math.ceil(Math.random()*10000));
		
		//if the iterable is an array, it will be used "as is"
		if(resultType.getSort()==Type.ARRAY)
		{
			//save array to a local variable
			int index_arr=varRegister.registerLocalVariable(randomName, resultType);
			visitor.visitLocalVariable(randomName, Type.INT_TYPE.getDescriptor(), null,  BytecodeInstructionsUtils.createAndVisitLabel(visitor), BytecodeInstructionsUtils.createAndVisitLabel(visitor), index_arr);
			visitor.visitInsn(Opcodes.DUP); //duplicate to get length after this
			visitor.visitVarInsn(resultType.getOpcode(Opcodes.ISTORE), index_arr);
			
			//end condition variable (array length)
			int index_end=varRegister.registerLocalVariable(randomName+"_end", Type.INT_TYPE);
			visitor.visitLocalVariable(randomName+"_end", Type.INT_TYPE.getDescriptor(), null,  BytecodeInstructionsUtils.createAndVisitLabel(visitor), BytecodeInstructionsUtils.createAndVisitLabel(visitor), index_end);
			visitor.visitInsn(Opcodes.ARRAYLENGTH);
			visitor.visitVarInsn(Type.INT_TYPE.getOpcode(Opcodes.ISTORE), index_end);
				
			//counter variable, initialized to 0
			int index=varRegister.registerLocalVariable(randomName+"_counter", Type.INT_TYPE);
			visitor.visitLocalVariable(randomName+"_counter", Type.INT_TYPE.getDescriptor(), null,  BytecodeInstructionsUtils.createAndVisitLabel(visitor), BytecodeInstructionsUtils.createAndVisitLabel(visitor), index);
			BytecodeInstructionsUtils.insertIntIntoStack(visitor, 0);
			visitor.visitVarInsn(Type.INT_TYPE.getOpcode(Opcodes.ISTORE), index);
			
			//condition
			forEachCondition=new BooleanCondition<Object>(ValueReference.forVariable(randomName+"_counter"), ValueReference.forVariable(randomName+"_end"), BooleanCondition.ConditionType.IS_LESS_THAN);
			
			//postExecution
			postExecution=(IConfigurableCodePart) ClassGeneratorFactory.getClassGenerator().lineFactory.variables.incrementVariable(randomName+"_counter");
			postExecution.configure(visitor, this, varRegister);
		}
		else
		{
//			//Cast to Iterable
//			visitor.visitTypeInsn(Opcodes.CHECKCAST, Type.getType(Iterable.class).getInternalName());

			//create iterator and save it to a local variable
			int index=varRegister.registerLocalVariable(randomName, Type.getType(Iterator.class));
			visitor.visitLocalVariable(randomName, Type.getDescriptor(Iterator.class), null, BytecodeInstructionsUtils.createAndVisitLabel(visitor), BytecodeInstructionsUtils.createAndVisitLabel(visitor), index);
			visitor.visitMethodInsn(Opcodes.INVOKEINTERFACE, Type.getInternalName(Iterable.class), "iterator",  Type.getMethodDescriptor(getMethod(Iterable.class, "iterator")));
			visitor.visitVarInsn(varRegister.getVariable(randomName).getType().getOpcode(Opcodes.ISTORE), index);

			//create Loop condition
			LineCodePart evaluateHasNext=ClassGeneratorFactory.getClassGenerator().lineFactory.methods.invokeMethod("hasNext", randomName, null, boolean.class);
			if(evaluateHasNext instanceof IConfigurableCodePart)
				((IConfigurableCodePart)evaluateHasNext).configure(visitor, this, varRegister);
			forEachCondition=new BooleanCondition<Object>(ValueReference.forExecutionResult(evaluateHasNext), BooleanCondition.ConditionType.IS_TRUE);
		}

		
		//labels
		startLabel=BytecodeInstructionsUtils.createAndVisitLabel(visitor);
		endLabel=new Label();
		postIterateLabel=new Label(); //no action needed, just go to start
		
		
		//loop code: At the start, evaluate if there are any more elements.
		//If there are no more, just jumps to end
		BytecodeInstructionsUtils.evaluateCondition(visitor, forEachCondition, endLabel, varRegister, parent);
		
		//create destination variable
		int indexLocalVar=varRegister.registerLocalVariable(variableName, Type.getType(variableType));
		visitor.visitLocalVariable(this.variableName, Type.getDescriptor(variableType), null, BytecodeInstructionsUtils.createAndVisitLabel(visitor), BytecodeInstructionsUtils.createAndVisitLabel(visitor), indexLocalVar);
		
		//get element from array or iterable and store into localVariable
		if(resultType.getSort()==Type.ARRAY)
		{
			BytecodeInstructionsUtils.insertArrayElementIntoStack(visitor, ValueReference.forVariable(randomName), ValueReference.forVariable(randomName+"_counter"), varRegister, this);
			visitor.visitVarInsn(Type.getType(variableType).getOpcode(Opcodes.ISTORE), indexLocalVar);			
		}
		else
		{
			//If there are more elements, peek one
			LineCodePart peekNext=ClassGeneratorFactory.getClassGenerator().lineFactory.methods.invokeMethodAndStore("next", randomName, null, Object.class, this.variableName);
			if(peekNext instanceof IConfigurableCodePart)
				((IConfigurableCodePart)peekNext).configure(visitor, this, varRegister);
			peekNext.generateBytecode();
		}
		
		//execute body
		if(this.bodyBlock!=null) this.bodyBlock.generateBytecode();
		
		visitor.visitLabel(postIterateLabel);
		if(postExecution!=null)
			postExecution.generateBytecode();
		

		//if code reach here, jump to start label to reevaluate condition
		visitor.visitJumpInsn(Opcodes.GOTO, startLabel);
				
		//visit end label, code jumps here on fail condition evaluation or "break" instruction
		visitor.visitLabel(endLabel);
	
		
	}

	private Method getMethod(Class<?> classObj, String name)
	{
		for(Method mth : classObj.getMethods())
		{
			if(mth.getName().equals(name))
				return mth;
		}
		throw new RuntimeException("Method "+name+" not found in class "+classObj.getName());
	}

	private void iterateByCondition() {
		//execute initial condition
		if(this.initialInstruction!=null) this.initialInstruction.generateBytecode();
		
		//Labels: start and end
		startLabel=BytecodeInstructionsUtils.createAndVisitLabel(visitor);
		endLabel=new Label();
		postIterateLabel=new Label();
		
		//evaluate condition
		if(condition==null)
		{
			//if there is no condition, create an "always-true" condition
			condition=new BooleanCondition<Boolean>(ValueReference.forStaticValue(Boolean.TRUE), BooleanCondition.ConditionType.IS_TRUE);
		}
		BytecodeInstructionsUtils.evaluateCondition(visitor, condition, endLabel, varRegister, parent);
		
		//generate body block
		if(this.bodyBlock!=null)
			this.bodyBlock.generateBytecode();
		
		//Visit postInstructions label, code jumps here on "continue" instruction
		visitor.visitLabel(postIterateLabel);
		
		if(this.postIterationInstruction!=null)
			this.postIterationInstruction.generateBytecode();
		
		//if code reach here, jump to start label to reevaluate condition
		visitor.visitJumpInsn(Opcodes.GOTO, startLabel);
		
		//visit end label, code jumps here on fail condition evaluation or "break" instruction
		visitor.visitLabel(endLabel);
	}
	
	
	

	@Override
	public boolean isReturnCodePart() {
		return false;
	}
	
	

	@Override
	public void configure(MethodVisitor visitor, AbstractCodeObject parent, VariableRegister varRegister)
	{
		this.visitor=visitor;
		this.parent=parent;
		this.varRegister=varRegister;
		
		if(this.initialInstruction!=null && this.initialInstruction instanceof IConfigurableCodePart)
			((IConfigurableCodePart)this.initialInstruction).configure(visitor, this, varRegister);
		if(this.postIterationInstruction!=null && this.postIterationInstruction instanceof IConfigurableCodePart)
			((IConfigurableCodePart)this.postIterationInstruction).configure(visitor, this, varRegister);
		if(this.bodyBlock!=null && this.bodyBlock instanceof IConfigurableCodePart)
			((IConfigurableCodePart)this.bodyBlock).configure(visitor, this, varRegister);
	
	}

	@Override
	public Label getStartLabel() {
		return this.startLabel;
	}

	@Override
	public Label getPostIterationLabel() {
		return this.postIterateLabel;
	}

	@Override
	public Label getEndLabel() {
		return this.endLabel;
	}

	public Class<?> getVariableType() {
		return variableType;
	}

	public void setVariableType(Class<?> variableType) {
		this.variableType = variableType;
	}

	public String getVariableName() {
		return variableName;
	}

	public void setVariableName(String variableName) {
		this.variableName = variableName;
	}

}
